package com.marketlive.app.b2c.session;

/*
 (C) Copyright MarketLive. 2006. All rights reserved.
 MarketLive is a trademark of MarketLive, Inc.
 Warning: This computer program is protected by copyright law and international treaties.
 Unauthorized reproduction or distribution of this program, or any portion of it, may result
 in severe civil and criminal penalties, and will be prosecuted to the maximum extent
 possible under the law.
 */

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.log4j.NDC;
import org.marketlive.biz.session.context.ICommerceSession;
import org.marketlive.biz.session.context.ICommerceSessionManager;
import org.marketlive.biz.session.context.IStartRequestExtension;
import org.marketlive.biz.session.context.IEndRequestExtension;
import org.marketlive.biz.account.IAccountManager;
import org.marketlive.entity.IPrimaryKey;
import org.marketlive.entity.account.ICustomer;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.WebApplicationContextUtils;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.sql.Timestamp;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.Locale;
import java.util.Set;
import java.util.Map;
import java.util.HashMap;

import com.marketlive.util.NetUtil;
import com.marketlive.util.StringUtil;
import com.marketlive.app.b2c.common.constants.RequestParams;
import com.marketlive.app.b2c.WebUtil;

/**
 * Servlet filter.
 */
public class CommerceSessionFilter implements javax.servlet.Filter {

    /** Quality-of-service threshold for WARN level. */
    private static final long QOS_WARN_THRESHHOLD = 2000;

    /** Quality-of-service threshold for ERROR level. */
    private static final long QOS_ERROR_THRESHHOLD = 4000;

    /** Path to the admin application. Requests under this prefix bypass CommerceSession. */
    public static final String ADMIN_PREFIX = "/admin";

    /** Name of cookie that stores customer id. */
    public static final String CUSTOMER_COOKIE_KEY = "customer";

    /** Name of cookie that stores basket id. */
    public static final String BASKET_COOKIE_KEY = "basket";

    /** Name of cookie that stores order id. */
    public static final String ORDER_COOKIE_KEY = "order";
    
    /** Name of cookie that stores the ABTest ID */
    public static final String ABTEST_COOKIE_KEY = "abtest";
    
    /** Name of cookie that stores the ABTestVersion ID */
    public static final String ABTEST_VERSION_COOKIE_KEY = "abtestversion";

    /** The number of seconds in a day. */
    public static final int SECONDS_PER_DAY = 24 * 60 * 60;

    /** Name of request parameter used to identify source codes. */
    public static final String SOURCE_CODE_KEY = "code";

    /** String equivalent to empty string. */
    private static final String EMPTY_STRING = "none";

    /** Thread-local variable to hold outermost HttpServletRequest. */
    private static final ThreadLocal REQUEST_CACHE = new ThreadLocal();

    /** Logger for this class. */
    private static Log log = LogFactory.getLog(CommerceSessionFilter.class);

    /** Commerce session manager for this class. */
    private ICommerceSessionManager commerceSessionManager;
    
    /**
     * Initializes this filter.
     *
     * @param filterConfig the <code>FilterConfig</code> to use for initializing this filter
     * @throws ServletException if an error occurs
     */
    public void init(FilterConfig filterConfig) throws ServletException {
        WebApplicationContext context = WebApplicationContextUtils.getRequiredWebApplicationContext(filterConfig
                .getServletContext());

        commerceSessionManager = (ICommerceSessionManager) context.getBean("commerceSessionManager");
    }

    /**
     * {@inheritDoc}
     */
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException,
            IOException {

        final HttpServletRequest httpRequest = (HttpServletRequest) request;

        if (log.isDebugEnabled()) {
            NDC.push("\t[" + httpRequest.getSession().getId() + "]\t");
        }

        final HttpServletResponse httpResponse = (HttpServletResponse) response;
        long startTime = System.currentTimeMillis();

        if (log.isDebugEnabled()) {
            log.debug("========================================================================");
            log.debug("start request: " + httpRequest.getRequestURI());

            Enumeration attrNames = request.getAttributeNames();

            while (attrNames.hasMoreElements()) {
                String attrName = (String) attrNames.nextElement();

                log.debug("\trequest attr " + attrName + ": " + request.getAttribute(attrName));
            }

            Enumeration paramNames = request.getParameterNames();

            while (paramNames.hasMoreElements()) {
                String paramName = (String) paramNames.nextElement();

                //Avoid logging PAN
                if ( paramName.equalsIgnoreCase("newCreditCard.number")){
                    String[] paramValues =  request.getParameterValues(paramName);
                    String paramValue = paramValues[0];
                    StringBuffer maskCC = new StringBuffer();
                    int length = paramValue.length();
                    maskCC.append("***********").append(paramValue.charAt(length-4)).append(paramValue.charAt(length-3)).append(paramValue.charAt(length-2)).append(paramValue.charAt(length-1)); 
                    
                    log.debug("\trequest param " + paramName + ": "
                            + maskCC.toString());

                }else if (! (paramName.equalsIgnoreCase("cvv2") || paramName.equalsIgnoreCase("loginPassword")||
                        paramName.equalsIgnoreCase("customer.hintAnswer") || paramName.equalsIgnoreCase("loginPasswordConfirm"))){
                    log.debug("\trequest param " + paramName + ": "
                            + StringUtil.arrayToString(request.getParameterValues(paramName)));
                }

            }
        }

        // check the current request for the the commerce session
        ICommerceSession commerceSession = getCommerceSession(httpRequest);

        // if not found, create one now
        if (commerceSession == null) {
            commerceSession = createCommerceSession(httpRequest);
            httpRequest.getSession().setAttribute(ICommerceSession.COMMERCE_SESSION, commerceSession);
        } else {
            // found, make sure last request was handle by the vm, or force a reload
            // of data
            // todo
            // make sure session always has a manager
            commerceSession.setCommerceSessionManager(commerceSessionManager);
        }

        // now that we have a commerce session, synchronize access to it.
        // these means that the server will only process one request per user
        // at a time.
        synchronized (commerceSession) {

            if (! WebUtil.isAdminApplicationRequest(httpRequest)) {

                // find the user's locale to use for display
                findUsersLocale(httpRequest,httpResponse);

                String pageUri = "";
                String uri = httpRequest.getRequestURI();
                if (uri == null || uri.length() < 1 || (uri.contains("/jump.do")
                && (httpRequest.getParameter(RequestParams.ITEM_TYPE).equalsIgnoreCase("ErrorPage")
                || httpRequest.getParameter(RequestParams.ITEM_TYPE).equalsIgnoreCase("Error404")))) {
                    pageUri = "/home.do";
                }
                else {
                    Locale aLocale = new Locale("en", "US");
                    String urlLocalePrefix = "/" + aLocale.toString();

                    if(uri.startsWith(urlLocalePrefix.toLowerCase())) {
                        pageUri = uri.substring(urlLocalePrefix.length());
                    }
                    else {
                        pageUri = uri;
                    }
                    pageUri = NetUtil.urlDecode(pageUri);
                    pageUri = NetUtil.urlEncode(pageUri);
                    pageUri = pageUri.replace("%2F","/");

                    StringBuffer queryString = new StringBuffer();

                    Enumeration paramNames = request.getParameterNames();
                    while (paramNames.hasMoreElements()) {
                        String paramName = (String) paramNames.nextElement();
                        if(paramName.equals(RequestParams.LOCALE))
                            continue;
                        String paramVal = StringUtil.arrayToString(request.getParameterValues(paramName));

                        if (queryString.length() > 0) {
                            queryString.append('&');
                        }

                        queryString.append(paramName);
                        queryString.append('=');
                        queryString.append(NetUtil.urlEncode(paramVal));
                    }
                    if(queryString.length() > 0)
                        pageUri = pageUri + "?" + queryString;
                }
                request.setAttribute("pageURI",pageUri);
            }

            // todo: move these into request start event
            commerceSessionManager.setCurrentRequest(httpRequest, commerceSession);
            triggerTrackerEvents(httpRequest, commerceSession);
            writeCookies(httpResponse, commerceSession);
            writeABTestCookies(httpResponse, commerceSession);

            commerceSessionManager.startRequest(commerceSession);

            startRequest(httpRequest, commerceSession);
            if (log.isDebugEnabled()) {
                logJSessionInfo(httpRequest, commerceSession);
            }


            // Store the request in the filter, but make sure only the outermost call
            // to this filter clears the request
            boolean clearRequestCache = false;
            if (REQUEST_CACHE.get() == null) {
                // store the request and remember we did it
                REQUEST_CACHE.set(request);
                clearRequestCache = true;
            }

            // bind request to thread
            try {
                chain.doFilter(request, response);
            } finally {
                if (clearRequestCache) {
                    // request was set on this pass through the method, so clear it
                    REQUEST_CACHE.set(null);
                }
            }
            
            endRequest(httpRequest, commerceSession);

    	    // Remove request from commerces session    
	    commerceSession.setCurrentRequest(null);

	}

        // Always check response times to verify quality of service.
        long requestTime = System.currentTimeMillis() - startTime;

        if (requestTime > QOS_ERROR_THRESHHOLD) {
            if (log.isInfoEnabled()) {
                log.info("QOS Error threshhold exceeded: [" + requestTime + "]" + httpRequest.getRequestURI());
            }
        } else if (requestTime > QOS_WARN_THRESHHOLD) {
            if (log.isInfoEnabled()) {
                log.info("QOS Warn threshhold exceeded: [" + requestTime + "]" + httpRequest.getRequestURI());
            }
        }

        if (log.isDebugEnabled()) {
            log.debug("requestTime: " + requestTime + " =========================================================");
            NDC.clear();
        }
        
    }

    /**
     * {@inheritDoc}
     */
    public void destroy() {

    }

    /**
     * Returns the top-level <code>HttpServletRequest</code>. In the case of nested templates, two requests are made.
     * This method returns the outermost, or original, request.
     *
     * @return the outermost request
     */
    public static HttpServletRequest getRequest() {
        return (HttpServletRequest) REQUEST_CACHE.get();
    }

    /**
     * Returns the commerce session or null if none has been assigned.
     *
     * @param request the request for which to get the commerce session
     * @return commerceSession for the given request
     */
    private ICommerceSession getCommerceSession(HttpServletRequest request) {
        return (ICommerceSession) request.getSession().getAttribute(ICommerceSession.COMMERCE_SESSION);
    }

    /**
     * Creates the commerce session. Includes checking for existing cookies and writing new ones as appropriate.
     *
     * @param httpRequest the request on which to create the commerce session
     * @return commerceSession the new commerce session
     */
    private ICommerceSession createCommerceSession(HttpServletRequest httpRequest) {
        // get the customer and basket cookies
        Cookie[] cookies = httpRequest.getCookies();

        String customerId = getCookieValue(cookies, CUSTOMER_COOKIE_KEY);
        String basketId = getCookieValue(cookies, BASKET_COOKIE_KEY);

        // if there is no basketId, check for one under ORDER_COOKIE_KEY for backwards compatibility
        if (basketId == null) {
            basketId = getCookieValue(cookies, ORDER_COOKIE_KEY);
        }

        ICommerceSession commerceSession = commerceSessionManager.createSession(customerId, basketId);
        
        String abtestID = getCookieValue(cookies, ABTEST_COOKIE_KEY);
        String abtestVersionID = getCookieValue(cookies, ABTEST_VERSION_COOKIE_KEY);
        commerceSession.setABTestID(abtestID);
        commerceSession.setABTestVersionID(abtestVersionID);
        
        return commerceSession;
    }

    /**
     * Returns the value of the specified cookie or null if none is defined.
     *
     * @param cookies the cookies to search
     * @param name the name of the cookie to find
     * @return value the value of the cookie with the given name
     */
    private String getCookieValue(Cookie[] cookies, String name) {

        if (cookies == null) {
            return null;
        }

        for (int i = 0; i < cookies.length; i++) {
            if (name.equals(cookies[i].getName())) {
                return cookies[i].getValue();
            }
        }

        return null;
    }

    /**
     * Adds the customer and basket cookies from the given commerce session to the given response.
     *
     * @param response the HTTP response to hold the cookies
     * @param commerceSession the commerce session from which to get the customer and basket cookies
     */
    private void writeCookies(HttpServletResponse response, ICommerceSession commerceSession) {

    	// ---------- Customer Cookie ---------> 
        IPrimaryKey customerPk = null;
        try {
            customerPk = commerceSession.getCustomerPk();
            String customerId;
            if (customerPk != null) {
                customerId = customerPk.getAsString();
            } else {
                customerId = EMPTY_STRING;
            }
            Cookie customerCookie = new Cookie(CUSTOMER_COOKIE_KEY, customerId);
            customerCookie.setPath("/");
            customerCookie.setMaxAge(commerceSessionManager.getCustomerDuration() * SECONDS_PER_DAY);
            response.addCookie(customerCookie);
        } catch (Exception e) {
            log.warn("Failed to write cookie for customer: " + customerPk, e);
        }      	
    }

    /**
     * Adds the ABTEst cookies from the given commerce session to the given response.
     *
     * @param response the HTTP response to hold the cookies
     * @param commerceSession the commerce session from which to get the ABTest cookies
     */
    private void writeABTestCookies(HttpServletResponse response, ICommerceSession commerceSession) {

    	// ---------- ABTest Cookie --------->
    	String abtestID = null;
        try {
            abtestID = commerceSession.getABTestID();
            int abtestDuration = (abtestID != null) ? commerceSession.getABTestDuration() : 0; // will remove cookie by default if no test IDs found in commerceSession
            Cookie abtestCookie = new Cookie(ABTEST_COOKIE_KEY, abtestID);
            abtestCookie.setPath("/");
            abtestCookie.setMaxAge(abtestDuration * SECONDS_PER_DAY);
            response.addCookie(abtestCookie);
        	String versionID = commerceSession.getABTestVersionID();
            Cookie versionCookie = new Cookie(ABTEST_VERSION_COOKIE_KEY, versionID);
            versionCookie.setPath("/");
            versionCookie.setMaxAge(abtestDuration * SECONDS_PER_DAY);
            response.addCookie(versionCookie);
        } catch (Exception e) {
            log.warn("Failed to write cookie for ABTest: " + abtestID, e);
        }
    }
    

    /**
     * Calls the session event tracker.
     *
     * @param request the HTTPServletRequest object for this context.
     * @param commerceSession the CommerceSession object for this context.
     */
    private void triggerTrackerEvents(HttpServletRequest request, ICommerceSession commerceSession) {
        //sessionTracker.logRequestStarted(commerceSession, request.getRequestURI(), request.getQueryString());
    }

    /**
     * log JSessionInfo.
     *
     */
    private void logJSessionInfo(HttpServletRequest request, ICommerceSession commerceSession) {

        String jsessionId= request.getSession().getId()!=null?request.getSession().getId().toLowerCase():"";
        String vmid_property = (System.getProperty("MARKETLIVE_VM_ID")!=null?System.getProperty("MARKETLIVE_VM_ID").trim().toLowerCase():"");

        log.debug("jsessionId " + jsessionId + " for cust ID: " + commerceSession.getCustomerPk() + " on VM_ID " + vmid_property );

        String firsCharJSID="";
        if(!"".equals(jsessionId) && !"".equals(vmid_property)){
            if ("1".equals(vmid_property))
                    firsCharJSID="a";
            else if ("2".equals(vmid_property))
                    firsCharJSID="b";
            else if ("3".equals(vmid_property))
                    firsCharJSID="c";
            else if ("4".equals(vmid_property))
                    firsCharJSID="d";
            else if ("5".equals(vmid_property))
                    firsCharJSID="e";
            else if ("6".equals(vmid_property))
                    firsCharJSID="f";
            else if ("7".equals(vmid_property))
                    firsCharJSID="g";
            else if ("8".equals(vmid_property))
                    firsCharJSID="h";
            else if ("9".equals(vmid_property))
                    firsCharJSID="i";
            else if ("10".equals(vmid_property))
                    firsCharJSID="j";

            log.debug("firsCharJSID " + firsCharJSID + " for cust ID: " + commerceSession.getCustomerPk() + " on VM_ID " + vmid_property );

            if (!jsessionId.startsWith(firsCharJSID)){
                log.error("VMJ_ERROR vm :" + vmid_property + " & jsession ID: " + jsessionId + " info does not match for " + commerceSession.getCustomerPk());
            }else{
                log.debug("VMJ_SUCCESS vm :" + vmid_property + " & jsession ID: " + jsessionId + " info matches for " + commerceSession.getCustomerPk());
            }
        }
    }



    public void findUsersLocale(HttpServletRequest request,HttpServletResponse resp) {

        //from CommerceSession
        Locale currentLocale = null;
        //locale set by this request
        Locale newLocale = null;

        String uri = request.getRequestURI();

        ICommerceSession commerceSession = getCommerceSession(request);
        if(commerceSession != null && commerceSession.getLocale() != null)
            currentLocale = commerceSession.getLocale();

        //get user's locale from a request parameter.
        String urlLocaleParameter = request.getParameter(RequestParams.LOCALE);
        Locale locale = getLocaleForLocaleCode(urlLocaleParameter);

        if(newLocale == null) {

            //try to get locale from the URI
            String[] uriParts = uri.substring(1).split("/",2);
            locale = getLocaleForLocaleCode(uriParts[0]);
        }

        if(newLocale == null) {
            //use locale in the commerce session.
            if(currentLocale != null) {
                newLocale = currentLocale;
            }
        }

        if(newLocale == null) {
            //determine locale from customer record, if available
            if(commerceSession != null) {
                ICustomer customer = commerceSession.getCustomer();
                if(customer != null && customer.getLocale() != null) {

                    locale = getLocaleForLocaleCode(customer.getLocale());
                }
            }
        }

        if(newLocale == null) {
            //determine user's locale based on the browser's locale
            if(request.getLocale() != null) {
                    newLocale = request.getLocale();
            }
        }

        if(newLocale == null) {
            //finally, fall back to the default locale for the current site.
            Locale defaultLocale = new Locale("en", "US");
            newLocale = defaultLocale;
        }

        //set selected locale in the commerce session.
        if(commerceSession != null) {
            commerceSession.setLocale(newLocale);
        }

        if(commerceSession != null && commerceSession.getCustomer() != null) {
            ICustomer customer = commerceSession.getCustomer();
            if(!newLocale.toString().equals(customer.getLocale())) {
                customer.setLocale(newLocale.toString());
                IAccountManager acctManager = commerceSession.getCommerceSessionManager().getAccountManager();
                acctManager.updateCustomer(customer);
            }
        }
    }

    private Locale getLocaleForLocaleCode(String localeCode) {
        if(localeCode != null) {
            if(localeCode.matches("..|.._..")) {

                if(localeCode.length() == 5) {

                    return new Locale(localeCode.substring(0,2),localeCode.substring(3));
                }
                else if (localeCode.length() == 2) {

                    return new Locale(localeCode.substring(0,2));
                }
            }
        }
        return null;
    }
    
    /**
     * Call the list of start request extensions.
     *
     * @param request the HTTPServetRequest object for this context.
     * @param commerceSession the CommerceSession object for this context.
     */
    private void startRequest(HttpServletRequest request, ICommerceSession commerceSession) {

        // Iterate through the list of start request extensions.
        Iterator iter = commerceSessionManager.getStartRequestExtensions().iterator();
        while (iter.hasNext()) {
            IStartRequestExtension startRequestExtension = (IStartRequestExtension) iter.next();
            startRequestExtension.startRequest(request, commerceSession);
        }
    }

    /**
     * Call the list of end request extensions.
     *
     * @param request the HTTPServetRequest object for this context.
     * @param commerceSession the CommerceSession object for this context.
     */
    private void endRequest(HttpServletRequest request, ICommerceSession commerceSession) {

        // Iterate through the list of start request extensions.
        Iterator iter = commerceSessionManager.getEndRequestExtensions().iterator();
        while (iter.hasNext()) {
            IEndRequestExtension endRequestExtension = (IEndRequestExtension) iter.next();
            endRequestExtension.endRequest(request, commerceSession);
        }
    }
}
